using System;

namespace PropertyExpression.Common
{
    public static class Option
    {
        public static IOption<TValue> Nothing<TValue>()
        {
            return NotExistOption<TValue>.Instance;
        }

        public static IOption<TValue> Nothing<TValue>(
            this TValue value)
        {
            return NotExistOption<TValue>.Instance;
        }

        public static IOption<TValue> AsOption<TValue>(
            this TValue value)
        {
            return new ExistOption<TValue>(value);
        }

        public static IOption<TTarget> Some<TValue, TTarget>(
            this IOption<TValue> option,
            Func<TValue, TTarget> func)
        {
            return PerformanceFix<TValue>.ProcessValue(option, func);
        }

        public static IOption<TTarget> Some<TValue, TTarget>(
            this IOption<TValue> option,
            Func<TValue, IOption<TTarget>> func)
        {
            return PerformanceFix<TValue>.ProcessValue(option, func);
        }

        public static void Process<TValue>(
            this IOption<TValue> option, 
            Action<TValue> existAction, 
            Action notExistAction)
        {
            PerformanceFix<TValue>.Process(option, existAction, notExistAction);
        }

        public static void Some<TValue>(
            this IOption<TValue> option, 
            Action<TValue> existAction)
        {
            PerformanceFix<TValue>.ProcessValue(option, existAction);
        }

        public static IOption<TTarget> Select<TValue, TTarget>(
            this IOption<TValue> option,
            Func<TValue, TTarget> func)
        {
            return option.Some(func);
        }

        public static IOption<TTarget> SelectMany<TValue, TTarget>(
            this IOption<TValue> option,
            Func<TValue, IOption<TTarget>> func)
        {
            return option.Some(func);
        }

        public static IOption<T2> SelectMany<TValue, T1, T2>(
            this IOption<TValue> option,
            Func<TValue, IOption<T1>> func1,
            Func<TValue, T1, T2> func2)
        {
            return option.SelectMany(
                value => func1(value).Select(
                    value1 => func2(value, value1)
                         )
                );
        }

        internal static TValue ValueOrEx2<TValue>(
            this IOption<TValue> option)
        {
            return PerformanceFix<TValue>.ValueOrEx(option);
        }

        public static TValue ValueOrDefault<TValue>(
            this IOption<TValue> option)
        {
            return PerformanceFix<TValue>.ValueOrDefault(option);
        }

        public static IOption<TValue> ToOptionalValue<TValue>(
            this TValue value) where TValue : class
        {
            return value == null ? Nothing<TValue>() : value.AsOption();
        }

        public static IOption<string> ToOptionIfNullOrEmpty(this string value)
        {
            return string.IsNullOrEmpty(value)
                       ? Nothing<string>()
                       : value.AsOption();
        }

        private static class PerformanceFix<TValue>
        {
            public static IOption<TTarget> ProcessValue<TTarget>(
                IOption<TValue> option,
                Func<TValue, TTarget> func)
            {
                if (option.HasValue)
                {
                    return func(option.Value).AsOption();
                }
                else
                {
                    return Nothing<TTarget>();
                }
            }

            public static IOption<TTarget> ProcessValue<TTarget>(
                IOption<TValue> option,
                Func<TValue, IOption<TTarget>> func)
            {
                return option.Process(func, Nothing<TTarget>);
            }

            public static void Process(
                IOption<TValue> option,
                Action<TValue> existAction,
                Action notExistAction)
            {
                option.Process(
                    value =>
                    {
                        existAction(value);
                        return new { };
                    },
                    () =>
                    {
                        notExistAction();
                        return new { };
                    }
                    );
            }

            public static void ProcessValue(
                IOption<TValue> option,
                Action<TValue> existAction)
            {
                Process(option, existAction, () => { });
            }

            public static TValue ValueOrEx(
                IOption<TValue> option)
            {
                return option.Process(
                    value => value,
                    () =>
                        {
                            throw new InvalidOperationException(
                                string.Format("Optional value of '{0}' type has no value.", typeof (TValue)));
                        }
                    );
            }

            public static TValue ValueOrDefault(
                IOption<TValue> option)
            {
                return option.Process(value => value, () => default(TValue));
            }
        }

        private class NotExistOption<TValue> : IOption<TValue>
        {
            public static readonly IOption<TValue> Instance = new NotExistOption<TValue>();

            private NotExistOption()
            {
            }

            public TResult Process<TResult>(
                Func<TValue, TResult> existFunc, Func<TResult> notExistFunc)
            {
                return notExistFunc();
            }

            public TValue Value
            {
                get { return this.ValueOrEx2(); }
            }

            public bool HasValue
            {
                get { return false; }
            }
        }

        private class ExistOption<TValue> : IOption<TValue>
        {
            private readonly TValue value;

            public ExistOption(TValue value)
            {
                this.value = value;
            }

            public TResult Process<TResult>(
                Func<TValue, TResult> existFunc, Func<TResult> notExistFunc)
            {
                return existFunc(value);
            }

            public TValue Value
            {
                get { return this.ValueOrEx2(); }
            }

            public bool HasValue
            {
                get { return true; }
            }
        }
    }
}